<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.

Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.module.ModuleManager</title>
<script src="../base.js"></script>
<script>
  goog.require('goog.array');
  goog.require('goog.functions');
  goog.require('goog.module.BaseModule');
  goog.require('goog.module.ModuleManager');
  goog.require('goog.module.ModuleManager.CallbackType');
  goog.require('goog.module.ModuleManager.FailureType');
  goog.require('goog.testing');
  goog.require('goog.testing.MockClock');
  goog.require('goog.testing.jsunit');
  goog.require('goog.testing.recordFunction');
</script>
</head>
<body>
<script>
  var clock;
  var requestCount = 0

  function setUpPage() {
    clock = new goog.testing.MockClock(true);
  }

  function tearDownPage() {
    clock.dispose();
  }

  function setUp() {
    requestCount = 0;
  }

  function getModuleManager(infoMap) {
    var mm = new goog.module.ModuleManager();
    mm.setAllModuleInfo(infoMap);

    mm.isModuleLoaded = function(id) {
      return this.getModuleInfo(id).isLoaded();
    }
    return mm;
  }

  function createSuccessfulBatchLoader(moduleMgr) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        requestCount++;
        setTimeout(goog.bind(this.onLoad, this, ids.concat(), 0), 5);
      },
      onLoad: function(ids, idxLoaded) {
        moduleMgr.beforeLoadModuleCode(ids[idxLoaded]);
        moduleMgr.setLoaded(ids[idxLoaded]);
        moduleMgr.afterLoadModuleCode(ids[idxLoaded]);
        var idx = idxLoaded + 1;
        if (idx < ids.length) {
          setTimeout(goog.bind(this.onLoad, this, ids, idx), 2);
        }
      }};
  }

  function createSuccessfulNonBatchLoader(moduleMgr) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        requestCount++;
        setTimeout(function() {
          moduleMgr.beforeLoadModuleCode(ids[0]);
          moduleMgr.setLoaded(ids[0]);
          moduleMgr.afterLoadModuleCode(ids[0]);
          if (opt_successFn) {
            opt_successFn();
          }
        }, 5);
      }};
  }

  function createUnsuccessfulLoader(moduleMgr, status) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        moduleMgr.beforeLoadModuleCode(ids[0]);
        setTimeout(function() { opt_errFn(status); }, 5);
      }};
  }

  function createUnsuccessfulBatchLoader(moduleMgr, status) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        setTimeout(function() { opt_errFn(status); }, 5);
      }};
  }

  function createTimeoutLoader(moduleMgr, status) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        setTimeout(function() { opt_timeoutFn(status); }, 5);
      }};
  }

  /**
   * Tests loading a module under different conditions i.e. unloaded
   * module, already loaded module, module loaded through user initiated
   * actions, synchronous callback for a module that has been already
   * loaded. Test both batch and non-batch loaders.
   */
  function testExecOnLoad() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));
    execOnLoad_(mm);

    mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulBatchLoader(mm));
    mm.setBatchModeEnabled(true);
    execOnLoad_(mm);
  }

  /**
   * Tests execOnLoad with the specified module manager.
   * @param {goog.module.ModuleManager} mm The module manager.
   */
  function execOnLoad_(mm) {
    // When module is unloaded, execOnLoad is async.
    var execCalled1 = false;
    mm.execOnLoad('a', function() { execCalled1 = true; });
    assertFalse('module "a" should not be loaded', mm.isModuleLoaded('a'));
    assertTrue('module "a" should be loading', mm.isModuleLoading('a'));
    assertFalse('execCalled1 should not be set yet', execCalled1);
    assertTrue('ModuleManager should be active', mm.isActive());
    assertFalse(
        'ModuleManager should not be user active', mm.isUserActive());
    clock.tick(5);
    assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
    assertFalse(
        'module "a" should not be loading', mm.isModuleLoading('a'));
    assertTrue('execCalled1 should be set', execCalled1);
    assertFalse('ModuleManager should not be active', mm.isActive());
    assertFalse(
        'ModuleManager should not be user active', mm.isUserActive());

    // When module is already loaded, execOnLoad is still async unless
    // specified otherwise.
    var execCalled2 = false;
    mm.execOnLoad('a', function() { execCalled2 = true; });
    assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
    assertFalse(
        'module "a" should not be loading', mm.isModuleLoading('a'));
    assertFalse('execCalled2 should not be set yet', execCalled2);
    clock.tick(5);
    assertTrue('execCalled2 should be set', execCalled2);

    // When module is unloaded, execOnLoad is async (user active).
    var execCalled5 = false;
    mm.execOnLoad('c',
        function() { execCalled5 = true; }, null, null, true);
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));
    assertTrue('module "c" should be loading', mm.isModuleLoading('c'));
    assertFalse('execCalled1 should not be set yet', execCalled5);
    assertTrue('ModuleManager should be active', mm.isActive());
    assertTrue('ModuleManager should be user active', mm.isUserActive());
    clock.tick(5);
    assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));
    assertFalse(
        'module "c" should not be loading', mm.isModuleLoading('c'));
    assertTrue('execCalled1 should be set', execCalled5);
    assertFalse('ModuleManager should not be active', mm.isActive());
    assertFalse(
        'ModuleManager should not be user active', mm.isUserActive());

    // When module is already loaded, execOnLoad is still synchronous when
    // so specified
    var execCalled6 = false;
    mm.execOnLoad('c', function() { execCalled6 = true; },
        undefined, undefined, undefined, true);
    assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));
    assertFalse(
        'module "c" should not be loading', mm.isModuleLoading('c'));
    assertTrue('execCalled6 should be set', execCalled6);
    clock.tick(5);
    assertTrue('execCalled6 should still be set', execCalled6);

  }

  /**
   * Test aborting the callback called on module load.
   */
  function testExecOnLoadAbort() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    // When module is unloaded and abort is called, module still gets
    // loaded, but callback is cancelled.
    var execCalled1 = false;
    var callback1 = mm.execOnLoad('b', function() { execCalled1 = true; });
    callback1.abort();
    clock.tick(5);
    assertTrue('module "b" should be loaded', mm.isModuleLoaded('b'));
    assertFalse('execCalled3 should not be set', execCalled1);

    // When module is already loaded, execOnLoad is still async, so calling
    // abort should still cancel the callback.
    var execCalled2 = false;
    var callback2 = mm.execOnLoad('a', function() { execCalled2 = true; });
    callback2.abort();
    clock.tick(5);
    assertFalse('execCalled2 should not be set', execCalled2);
  }

  /**
   * Test preloading modules and ensure that the before load, after load
   * and set load called are called only once per module.
   */
  function testExecOnLoadWhilePreloadingAndViceVersa() {
    var mm = getModuleManager({'c': [], 'd': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));
    execOnLoadWhilePreloadingAndViceVersa_(mm);

    mm = getModuleManager({'c': [], 'd': []});
    mm.setLoader(createSuccessfulBatchLoader(mm));
    mm.setBatchModeEnabled(true);
    execOnLoadWhilePreloadingAndViceVersa_(mm);
  }

  /**
   * Perform tests with the specified module manager.
   * @param {goog.module.ModuleManager} mm The module manager.
   */
  function execOnLoadWhilePreloadingAndViceVersa_(mm) {
    var mm = getModuleManager({'c': [], 'd': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var origSetLoaded = mm.setLoaded;
    var calls = [0, 0, 0];
    mm.beforeLoadModuleCode = function(id) {
      calls[0]++;
    };
    mm.setLoaded = function(id) {
      calls[1]++;
      origSetLoaded.call(mm, id);
    };
    mm.afterLoadModuleCode = function(id) {
      calls[2]++;
    };

    mm.preloadModule('c', 2);
    assertFalse(
        'module "c" should not be loading yet', mm.isModuleLoading('c'));
    clock.tick(2);
    assertTrue(
        'module "c" should now be loading', mm.isModuleLoading('c'));
    mm.execOnLoad('c', function() {});
    assertTrue(
        'module "c" should still be loading', mm.isModuleLoading('c'));
    clock.tick(5);
    assertFalse(
        'module "c" should be done loading', mm.isModuleLoading('c'));
    assertEquals(
        'beforeLoad should only be called once for "c"', 1, calls[0]);
    assertEquals(
        'setLoaded should only be called once for "c"', 1, calls[1]);
    assertEquals(
        'afterLoad should only be called once for "c"', 1, calls[2]);

    mm.execOnLoad('d', function() {});
    assertTrue(
        'module "d" should now be loading', mm.isModuleLoading('d'));
    mm.preloadModule('d', 2);
    clock.tick(5);
    assertFalse(
        'module "d" should be done loading', mm.isModuleLoading('d'));
    assertTrue(
        'module "d" should now be loaded', mm.isModuleLoaded('d'));
    assertEquals(
        'beforeLoad should only be called once for "d"', 2, calls[0]);
    assertEquals(
        'setLoaded should only be called once for "d"', 2, calls[1]);
    assertEquals(
        'afterLoad should only be called once for "d"', 2, calls[2]);
  }

  /**
   * Tests that multiple callbacks on the same module don't cause
   * confusion about the active state after the module is finally loaded.
   */
  function testUserInitiatedExecOnLoadEventuallyLeavesManagerIdle() {
    var mm = getModuleManager({'c': [], 'd': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack1 = false;
    var calledBack2 = false;

    mm.execOnLoad(
        'c',
        function() {
          calledBack1 = true;
        },
        undefined,
        undefined,
        true);
    mm.execOnLoad(
        'c',
        function() {
          calledBack2 = true;
        },
        undefined,
        undefined,
        true);
    mm.load('c');

    assertTrue(
        'Manager should be active while waiting for load', mm.isUserActive());

    clock.tick(5);

    assertTrue('First callback should be called', calledBack1);
    assertTrue('Second callback should be called', calledBack2);
    assertFalse(
        'Manager should be inactive after loading is complete',
        mm.isUserActive());
  }

  /**
   * Tests loading a module by requesting a Deferred object.
   */
  function testLoad() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;
    var error = null;

    var d = mm.load('a');
    d.addCallback(function(ctx) {
      calledBack = true;
    });
    d.addErrback(function(err) {
      error = err;
    });

    assertFalse(calledBack);
    assertNull(error);
    assertFalse(mm.isUserActive());

    clock.tick(5);

    assertTrue(calledBack);
    assertNull(error);
  }

  /**
   * Tests loading 2 modules asserting that the loads happen in parallel
   * in one unit of time.
   */
  function testLoad_concurrent() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setConcurrentLoadingEnabled(true);
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;
    var error = null;

    mm.load('a');
    mm.load('b');
    assertEquals(2, requestCount);
    // Only time for one serialized download.
    clock.tick(5);

    assertTrue(mm.getModuleInfo('a').isLoaded());
    assertTrue(mm.getModuleInfo('b').isLoaded());
  }

  function testLoad_concurrentSecondIsDepOfFist() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setBatchModeEnabled(true);
    mm.setConcurrentLoadingEnabled(true);
    mm.setLoader(createSuccessfulBatchLoader(mm));

    var calledBack = false;
    var error = null;

    mm.loadMultiple(['a', 'b']);
    mm.load('b');
    assertEquals('No 2nd request expected', 1, requestCount);
    // Only time for one serialized download.
    clock.tick(5);
    clock.tick(2); // Makes second module come in from batch requst.

    assertTrue(mm.getModuleInfo('a').isLoaded());
    assertTrue(mm.getModuleInfo('b').isLoaded());
  }

  function testLoad_nonConcurrent() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;
    var error = null;

    mm.load('a');
    mm.load('b');
    assertEquals(1, requestCount);
    // Only time for one serialized download.
    clock.tick(5);

    assertTrue(mm.getModuleInfo('a').isLoaded());
    assertFalse(mm.getModuleInfo('b').isLoaded());
  }

  function testLoadUnknown() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));
    var e = assertThrows(function() {
      mm.load('DoesNotExist');
    });
    assertEquals('Unknown module: DoesNotExist', e.message);
  }

  /**
   * Tests loading multiple modules by requesting a Deferred object.
   */
  function testLoadMultiple() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setBatchModeEnabled(true);
    mm.setLoader(createSuccessfulBatchLoader(mm));

    var calledBack = false;
    var error = null;
    var calledBack2 = false;
    var error2 = null;

    var dMap = mm.loadMultiple(['a', 'b']);
    dMap['a'].addCallback(function(ctx) {
      calledBack = true;
    });
    dMap['a'].addErrback(function(err) {
      error = err;
    });
    dMap['b'].addCallback(function(ctx) {
      calledBack2 = true;
    });
    dMap['b'].addErrback(function(err) {
      error2 = err;
    });

    assertFalse(calledBack);
    assertFalse(calledBack2);

    clock.tick(5);
    assertTrue(calledBack);
    assertFalse(calledBack2);
    assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
    assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

    clock.tick(2);

    assertTrue(calledBack);
    assertTrue(calledBack2);
    assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
    assertTrue('module "b" should be loaded', mm.isModuleLoaded('b'));
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));
    assertNull(error);
    assertNull(error2);
  }

  /**
   * Tests loading multiple modules with deps by requesting a Deferred object.
   */
  function testLoadMultipleWithDeps() {
    var mm = getModuleManager({'a': [], 'b': ['c'], 'c': []});
    mm.setBatchModeEnabled(true);
    mm.setLoader(createSuccessfulBatchLoader(mm));

    var calledBack = false;
    var error = null;
    var calledBack2 = false;
    var error2 = null;

    var dMap = mm.loadMultiple(['a', 'b']);
    dMap['a'].addCallback(function(ctx) {
      calledBack = true;
    });
    dMap['a'].addErrback(function(err) {
      error = err;
    });
    dMap['b'].addCallback(function(ctx) {
      calledBack2 = true;
    });
    dMap['b'].addErrback(function(err) {
      error2 = err;
    });

    assertFalse(calledBack);
    assertFalse(calledBack2);

    clock.tick(5);
    assertTrue(calledBack);
    assertFalse(calledBack2);
    assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
    assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

    clock.tick(2);

    assertFalse(calledBack2);
    assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
    assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
    assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));

    clock.tick(2);

    assertTrue(calledBack2);
    assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
    assertTrue('module "b" should be loaded', mm.isModuleLoaded('b'));
    assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));
    assertNull(error);
    assertNull(error2);
  }

  /**
   * Tests loading multiple modules by requesting a Deferred object when
   * a server error occurs.
   */
  function testLoadMultipleWithErrors() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setBatchModeEnabled(true);
    mm.setLoader(createUnsuccessfulLoader(mm, 500));

    var calledBack = false;
    var error = null;
    var calledBack2 = false;
    var error2 = null;
    var calledBack3 = false;
    var error3 = null;

    var dMap = mm.loadMultiple(['a', 'b', 'c']);
    dMap['a'].addCallback(function(ctx) {
      calledBack = true;
    });
    dMap['a'].addErrback(function(err) {
      error = err;
    });
    dMap['b'].addCallback(function(ctx) {
      calledBack2 = true;
    });
    dMap['b'].addErrback(function(err) {
      error2 = err;
    });
    dMap['c'].addCallback(function(ctx) {
      calledBack3 = true;
    });
    dMap['c'].addErrback(function(err) {
      error3 = err;
    });

    assertFalse(calledBack);
    assertFalse(calledBack2);
    assertFalse(calledBack3);

    clock.tick(4);

    // A module request is now underway using the unsuccessful loader.
    // We substitute a successful loader for future module load requests.
    mm.setLoader(createSuccessfulBatchLoader(mm));

    clock.tick(1);

    assertFalse(calledBack);
    assertFalse(calledBack2);
    assertFalse(calledBack3);
    assertFalse('module "a" should not be loaded', mm.isModuleLoaded('a'));
    assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

    // Retry should happen after a backoff
    clock.tick(5 + mm.getBackOff_());

    assertTrue(calledBack);
    assertFalse(calledBack2);
    assertFalse(calledBack3);
    assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
    assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

    clock.tick(2);
    assertTrue(calledBack2);
    assertFalse(calledBack3);
    assertTrue('module "b" should be loaded', mm.isModuleLoaded('b'));
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

    clock.tick(2);
    assertTrue(calledBack3);
    assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));

    assertNull(error);
    assertNull(error2);
    assertNull(error3);
  }

  /**
   * Tests loading multiple modules by requesting a Deferred object when
   * consecutive server error occur and the loader falls back to serial
   * loads.
   */
  function testLoadMultipleWithErrorsFallbackOnSerial() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setBatchModeEnabled(true);
    mm.setLoader(createUnsuccessfulLoader(mm, 500));

    var calledBack = false;
    var error = null;
    var calledBack2 = false;
    var error2 = null;
    var calledBack3 = false;
    var error3 = null;

    var dMap = mm.loadMultiple(['a', 'b', 'c']);
    dMap['a'].addCallback(function(ctx) {
      calledBack = true;
    });
    dMap['a'].addErrback(function(err) {
      error = err;
    });
    dMap['b'].addCallback(function(ctx) {
      calledBack2 = true;
    });
    dMap['b'].addErrback(function(err) {
      error2 = err;
    });
    dMap['c'].addCallback(function(ctx) {
      calledBack3 = true;
    });
    dMap['c'].addErrback(function(err) {
      error3 = err;
    });

    assertFalse(calledBack);
    assertFalse(calledBack2);
    assertFalse(calledBack3);

    clock.tick(5);

    assertFalse(calledBack);
    assertFalse(calledBack2);
    assertFalse(calledBack3);
    assertFalse('module "a" should not be loaded', mm.isModuleLoaded('a'));
    assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

    // Retry should happen and fail after a backoff
    clock.tick(5 + mm.getBackOff_());
    assertFalse(calledBack);
    assertFalse(calledBack2);
    assertFalse(calledBack3);
    assertFalse('module "a" should not be loaded', mm.isModuleLoaded('a'));
    assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

    // A second retry should happen after a backoff
    clock.tick(4 + mm.getBackOff_());
    // The second retry is now underway using the unsuccessful loader.
    // We substitute a successful loader for future module load requests.
    mm.setLoader(createSuccessfulBatchLoader(mm));

    clock.tick(1);

    // A second retry should fail now
    assertFalse(calledBack);
    assertFalse(calledBack2);
    assertFalse(calledBack3);
    assertFalse('module "a" should not be loaded', mm.isModuleLoaded('a'));
    assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

    // Each module should be loaded individually now, each taking 5 ticks

    clock.tick(5);
    assertTrue(calledBack);
    assertFalse(calledBack2);
    assertFalse(calledBack3);
    assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
    assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

    clock.tick(5);
    assertTrue(calledBack2);
    assertFalse(calledBack3);
    assertTrue('module "b" should be loaded', mm.isModuleLoaded('b'));
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

    clock.tick(5);
    assertTrue(calledBack3);
    assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));

    assertNull(error);
    assertNull(error2);
    assertNull(error3);
  }

  /**
   * Tests loading a module by user action by requesting a Deferred object.
   */
  function testLoadForUser() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;
    var error = null;

    var d = mm.load('a', true);
    d.addCallback(function(ctx) {
      calledBack = true;
    });
    d.addErrback(function(err) {
      error = err;
    });

    assertFalse(calledBack);
    assertNull(error);
    assertTrue(mm.isUserActive());

    clock.tick(5);

    assertTrue(calledBack);
    assertNull(error);
  }

  /**
   * Tests that preloading a module calls back the deferred object.
   */
  function testPreloadDeferredWhenNotLoaded() {
    var mm = getModuleManager({'a': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;

    var d = mm.preloadModule('a');;
    d.addCallback(function(ctx) {
      calledBack = true;
    });

    // First load should take five ticks.
    assertFalse('module "a" should not be loaded yet', calledBack);
    clock.tick(5);
    assertTrue('module "a" should be loaded', calledBack);
  }

  /**
   * Tests preloading an already loaded module.
   */
  function testPreloadDeferredWhenLoaded() {
    var mm = getModuleManager({'a': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;

    mm.preloadModule('a');
    clock.tick(5);

    var d = mm.preloadModule('a');;
    d.addCallback(function(ctx) {
      calledBack = true;
    });

    // Module is already loaded, should be called back after the setTimeout
    // in preloadModule.
    assertFalse('deferred for module "a" should not be called yet', calledBack);
    clock.tick(1);
    assertTrue('module "a" should be loaded', calledBack);
  }


  /**
   * Tests preloading a module that is currently loading.
   */
  function testPreloadDeferredWhenLoading() {
    var mm = getModuleManager({'a': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    mm.preloadModule('a');
    clock.tick(1);

    // 'b' is in the middle of loading, should get called back when it's done.
    var calledBack = false;
    var d = mm.preloadModule('a');;
    d.addCallback(function(ctx) {
      calledBack = true;
    });

    assertFalse('module "a" should not be loaded yet', calledBack);
    clock.tick(4);
    assertTrue('module "a" should be loaded', calledBack);
  }

  /**
   * Tests that load doesn't trigger another load if a module is already
   * preloading.
   */
  function testLoadWhenPreloading() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var origSetLoaded = mm.setLoaded;
    var calls = [0, 0, 0];
    mm.beforeLoadModuleCode = function(id) {
      calls[0]++;
    };
    mm.setLoaded = function(id) {
      calls[1]++;
      origSetLoaded.call(mm, id);
    };
    mm.afterLoadModuleCode = function(id) {
      calls[2]++;
    };

    var calledBack = false;
    var error = null;

    mm.preloadModule('c', 2);
    assertFalse(
        'module "c" should not be loading yet', mm.isModuleLoading('c'));
    clock.tick(2);
    assertTrue(
        'module "c" should now be loading', mm.isModuleLoading('c'));

    var d = mm.load('c');
    d.addCallback(function(ctx) {
      calledBack = true;
    });
    d.addErrback(function(err) {
      error = err;
    });

    assertTrue(
        'module "c" should still be loading', mm.isModuleLoading('c'));
    clock.tick(5);
    assertFalse(
        'module "c" should be done loading', mm.isModuleLoading('c'));
    assertEquals(
        'beforeLoad should only be called once for "c"', 1, calls[0]);
    assertEquals(
        'setLoaded should only be called once for "c"', 1, calls[1]);
    assertEquals(
        'afterLoad should only be called once for "c"', 1, calls[2]);

    assertTrue(calledBack);
    assertNull(error);
  }

  /**
   * Tests that load doesn't trigger another load if a module is already
   * preloading.
   */
  function testLoadMultipleWhenPreloading() {
    var mm = getModuleManager({'a': [], 'b': ['d'], 'c': [], 'd': []});
    mm.setLoader(createSuccessfulBatchLoader(mm));
    mm.setBatchModeEnabled(true);

    var origSetLoaded = mm.setLoaded;
    var calls = {'a': [0, 0, 0], 'b': [0, 0, 0],
                 'c': [0, 0, 0], 'd': [0, 0, 0]};
    mm.beforeLoadModuleCode = function(id) {
      calls[id][0]++;
    };
    mm.setLoaded = function(id) {
      calls[id][1]++;
      origSetLoaded.call(mm, id);
    };
    mm.afterLoadModuleCode = function(id) {
      calls[id][2]++;
    };

    var calledBack = false;
    var error = null;
    var calledBack2 = false;
    var error2 = null;
    var calledBack3 = false;
    var error3 = null;

    mm.preloadModule('c', 2);
    mm.preloadModule('d', 3);
    assertFalse(
        'module "c" should not be loading yet', mm.isModuleLoading('c'));
    assertFalse(
        'module "d" should not be loading yet', mm.isModuleLoading('d'));
    clock.tick(2);
    assertTrue(
        'module "c" should now be loading', mm.isModuleLoading('c'));
    clock.tick(1);
    assertTrue(
        'module "d" should now be loading', mm.isModuleLoading('d'));

    var dMap = mm.loadMultiple(['a', 'b', 'c']);
    dMap['a'].addCallback(function(ctx) {
      calledBack = true;
    });
    dMap['a'].addErrback(function(err) {
      error = err;
    });
    dMap['b'].addCallback(function(ctx) {
      calledBack2 = true;
    });
    dMap['b'].addErrback(function(err) {
      error2 = err;
    });
    dMap['c'].addCallback(function(ctx) {
      calledBack3 = true;
    });
    dMap['c'].addErrback(function(err) {
      error3 = err;
    });

    assertTrue(
        'module "a" should be loading', mm.isModuleLoading('a'));
    assertTrue(
        'module "b" should be loading', mm.isModuleLoading('b'));
    assertTrue(
        'module "c" should still be loading', mm.isModuleLoading('c'));
    clock.tick(4);
    assertTrue(calledBack3);

    assertFalse(
        'module "c" should be done loading', mm.isModuleLoading('c'));
    assertTrue(
        'module "d" should still be loading', mm.isModuleLoading('d'));
    clock.tick(5);
    assertFalse(
        'module "d" should be done loading', mm.isModuleLoading('d'));

    assertFalse(calledBack);
    assertFalse(calledBack2);
    assertTrue(
        'module "a" should still be loading', mm.isModuleLoading('a'));
    assertTrue(
        'module "b" should still be loading', mm.isModuleLoading('b'));
    clock.tick(7);

    assertTrue(calledBack);
    assertTrue(calledBack2);
    assertFalse(
        'module "a" should be done loading', mm.isModuleLoading('a'));
    assertFalse(
        'module "b" should be done loading', mm.isModuleLoading('b'));

    assertEquals(
        'beforeLoad should only be called once for "a"', 1, calls['a'][0]);
    assertEquals(
        'setLoaded should only be called once for "a"', 1, calls['a'][1]);
    assertEquals(
        'afterLoad should only be called once for "a"', 1, calls['a'][2]);
    assertEquals(
        'beforeLoad should only be called once for "b"', 1, calls['b'][0]);
    assertEquals(
        'setLoaded should only be called once for "b"', 1, calls['b'][1]);
    assertEquals(
        'afterLoad should only be called once for "b"', 1, calls['b'][2]);
    assertEquals(
        'beforeLoad should only be called once for "c"', 1, calls['c'][0]);
    assertEquals(
        'setLoaded should only be called once for "c"', 1, calls['c'][1]);
    assertEquals(
        'afterLoad should only be called once for "c"', 1, calls['c'][2]);
    assertEquals(
        'beforeLoad should only be called once for "d"', 1, calls['d'][0]);
    assertEquals(
        'setLoaded should only be called once for "d"', 1, calls['d'][1]);
    assertEquals(
        'afterLoad should only be called once for "d"', 1, calls['d'][2]);

    assertNull(error);
    assertNull(error2);
    assertNull(error3);
  }

/**
   * Tests that the deferred is still called when loadMultiple loads modules
   * that are already preloading.
   */
  function testLoadMultipleWhenPreloadingSameModules() {
    var mm = getModuleManager({'a': [], 'b': ['d'], 'c': [], 'd': []});
    mm.setLoader(createSuccessfulBatchLoader(mm));
    mm.setBatchModeEnabled(true);

    var origSetLoaded = mm.setLoaded;
    var calls = {'c': [0, 0, 0], 'd': [0, 0, 0]};
    mm.beforeLoadModuleCode = function(id) {
      calls[id][0]++;
    };
    mm.setLoaded = function(id) {
      calls[id][1]++;
      origSetLoaded.call(mm, id);
    };
    mm.afterLoadModuleCode = function(id) {
      calls[id][2]++;
    };

    var calledBack = false;
    var error = null;
    var calledBack2 = false;
    var error2 = null;

    mm.preloadModule('c', 2);
    mm.preloadModule('d', 3);
    assertFalse(
        'module "c" should not be loading yet', mm.isModuleLoading('c'));
    assertFalse(
        'module "d" should not be loading yet', mm.isModuleLoading('d'));
    clock.tick(2);
    assertTrue(
        'module "c" should now be loading', mm.isModuleLoading('c'));
    clock.tick(1);
    assertTrue(
        'module "d" should now be loading', mm.isModuleLoading('d'));

    var dMap = mm.loadMultiple(['c', 'd']);
    dMap['c'].addCallback(function(ctx) {
      calledBack = true;
    });
    dMap['c'].addErrback(function(err) {
      error = err;
    });
    dMap['d'].addCallback(function(ctx) {
      calledBack2 = true;
    });
    dMap['d'].addErrback(function(err) {
      error2 = err;
    });

    assertTrue(
        'module "c" should still be loading', mm.isModuleLoading('c'));
    clock.tick(4);
    assertFalse(
        'module "c" should be done loading', mm.isModuleLoading('c'));
    assertTrue(
        'module "d" should still be loading', mm.isModuleLoading('d'));
    clock.tick(5);
    assertFalse(
        'module "d" should be done loading', mm.isModuleLoading('d'));

    assertTrue(calledBack);
    assertTrue(calledBack2);

    assertEquals(
        'beforeLoad should only be called once for "c"', 1, calls['c'][0]);
    assertEquals(
        'setLoaded should only be called once for "c"', 1, calls['c'][1]);
    assertEquals(
        'afterLoad should only be called once for "c"', 1, calls['c'][2]);
    assertEquals(
        'beforeLoad should only be called once for "d"', 1, calls['d'][0]);
    assertEquals(
        'setLoaded should only be called once for "d"', 1, calls['d'][1]);
    assertEquals(
        'afterLoad should only be called once for "d"', 1, calls['d'][2]);

    assertNull(error);
    assertNull(error2);
  }

 /**
   * Tests loading a module via load when the module is already
   * loaded.  The deferred's callback should be called immediately.
   */
 function testLoadWhenLoaded() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;
    var error = null;

    mm.preloadModule('b', 2);
    clock.tick(10);

    assertFalse(
        'module "b" should be done loading', mm.isModuleLoading('b'));

    var d = mm.load('b');
    d.addCallback(function(ctx) {
      calledBack = true;
    });
    d.addErrback(function(err) {
      error = err;
    });

    assertTrue(calledBack);
    assertNull(error);
  }

  /**
   * Tests that the deferred's errbacks are called if the module fails to load.
   */
  function testLoadWithFailingModule() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createUnsuccessfulLoader(mm, 401));
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
      assertEquals('Failure cause was not as expected',
                   goog.module.ModuleManager.FailureType.UNAUTHORIZED,
                   cause);
      firedLoadFailed = true;
    });
    var calledBack = false;
    var error = null;

    var d = mm.load('a');
    d.addCallback(function(ctx) {
      calledBack = true;
    });
    d.addErrback(function(err) {
      error = err;
    });

    assertFalse(calledBack);
    assertNull(error);

    clock.tick(500);

    assertFalse(calledBack);

    // NOTE: Deferred always calls errbacks with an Error object.  For now the
    // module manager just passes the FailureType which gets set as the Error
    // object's message.
    assertEquals('Failure cause was not as expected',
        goog.module.ModuleManager.FailureType.UNAUTHORIZED,
        Number(error.message));
  }

  /**
   * Tests that the deferred's errbacks are called if a module fails to load.
   */
  function testLoadMultipleWithFailingModule() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createUnsuccessfulLoader(mm, 401));
    mm.setBatchModeEnabled(true);
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
      assertEquals('Failure cause was not as expected',
                   goog.module.ModuleManager.FailureType.UNAUTHORIZED,
                   cause);
    });
    var calledBack11 = false;
    var error11 = null;
    var calledBack12 = false;
    var error12 = null;
    var calledBack21 = false;
    var error21 = null;
    var calledBack22 = false;
    var error22 = null;

    var dMap = mm.loadMultiple(['a', 'b']);
    dMap['a'].addCallback(function(ctx) {
      calledBack11 = true;
    });
    dMap['a'].addErrback(function(err) {
      error11 = err;
    });
    dMap['b'].addCallback(function(ctx) {
      calledBack12 = true;
    });
    dMap['b'].addErrback(function(err) {
      error12 = err;
    });

    var dMap2 = mm.loadMultiple(['b', 'c']);
    dMap2['b'].addCallback(function(ctx) {
      calledBack21 = true;
    });
    dMap2['b'].addErrback(function(err) {
      error21 = err;
    });
    dMap2['c'].addCallback(function(ctx) {
      calledBack22 = true;
    });
    dMap2['c'].addErrback(function(err) {
      error22 = err;
    });

    assertFalse(calledBack11);
    assertFalse(calledBack12);
    assertFalse(calledBack21);
    assertFalse(calledBack22);
    assertNull(error11);
    assertNull(error12);
    assertNull(error21);
    assertNull(error22);

    clock.tick(5);

    assertFalse(calledBack11);
    assertFalse(calledBack12);
    assertFalse(calledBack21);
    assertFalse(calledBack22);

    // NOTE: Deferred always calls errbacks with an Error object.  For now the
    // module manager just passes the FailureType which gets set as the Error
    // object's message.
    assertEquals('Failure cause was not as expected',
        goog.module.ModuleManager.FailureType.UNAUTHORIZED,
        Number(error11.message));
    assertEquals('Failure cause was not as expected',
        goog.module.ModuleManager.FailureType.UNAUTHORIZED,
        Number(error12.message));

    // The first deferred of the second load should be called since it asks for
    // one of the failed modules.
    assertEquals('Failure cause was not as expected',
        goog.module.ModuleManager.FailureType.UNAUTHORIZED,
        Number(error21.message));

    // The last deferred should be dropped so it is neither called back nor an
    // error.
    assertFalse(calledBack22);
    assertNull(error22);
  }

  /**
   * Tests that the right dependencies are cancelled on a loadMultiple failure.
   */
  function testLoadMultipleWithFailingModuleDependencies() {
    var mm = getModuleManager(
        {'a': [], 'b': [], 'c': ['b'], 'd': ['c'], 'e': []});
    mm.setLoader(createUnsuccessfulLoader(mm, 401));
    mm.setBatchModeEnabled(true);
    var cancelledIds = [];

    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
      assertEquals('Failure cause was not as expected',
                   goog.module.ModuleManager.FailureType.UNAUTHORIZED,
                   cause);
      cancelledIds.push(id);
    });
    var calledBack11 = false;
    var error11 = null;
    var calledBack12 = false;
    var error12 = null;
    var calledBack21 = false;
    var error21 = null;
    var calledBack22 = false;
    var error22 = null;
    var calledBack23 = false;
    var error23 = null;

    var dMap = mm.loadMultiple(['a', 'b']);
    dMap['a'].addCallback(function(ctx) {
      calledBack11 = true;
    });
    dMap['a'].addErrback(function(err) {
      error11 = err;
    });
    dMap['b'].addCallback(function(ctx) {
      calledBack12 = true;
    });
    dMap['b'].addErrback(function(err) {
      error12 = err;
    });

    var dMap2 = mm.loadMultiple(['c', 'd', 'e']);
    dMap2['c'].addCallback(function(ctx) {
      calledBack21 = true;
    });
    dMap2['c'].addErrback(function(err) {
      error21 = err;
    });
    dMap2['d'].addCallback(function(ctx) {
      calledBack22 = true;
    });
    dMap2['d'].addErrback(function(err) {
      error22 = err;
    });
    dMap2['e'].addCallback(function(ctx) {
      calledBack23 = true;
    });
    dMap2['e'].addErrback(function(err) {
      error23 = err;
    });

    assertFalse(calledBack11);
    assertFalse(calledBack12);
    assertFalse(calledBack21);
    assertFalse(calledBack22);
    assertFalse(calledBack23);
    assertNull(error11);
    assertNull(error12);
    assertNull(error21);
    assertNull(error22);
    assertNull(error23);

    clock.tick(5);

    assertFalse(calledBack11);
    assertFalse(calledBack12);
    assertFalse(calledBack21);
    assertFalse(calledBack22);
    assertFalse(calledBack23);

    // NOTE: Deferred always calls errbacks with an Error object.  For now the
    // module manager just passes the FailureType which gets set as the Error
    // object's message.
    assertEquals('Failure cause was not as expected',
        goog.module.ModuleManager.FailureType.UNAUTHORIZED,
        Number(error11.message));
    assertEquals('Failure cause was not as expected',
        goog.module.ModuleManager.FailureType.UNAUTHORIZED,
        Number(error12.message));

    // Check that among the failed modules, 'c' and 'd' are also cancelled
    // due to dependencies.
    assertTrue(goog.array.equals(['a', 'b', 'c', 'd'], cancelledIds.sort()));
  }

  /**
   * Tests that when loading multiple modules, the input array is not modified
   * when it has duplicates.
   */
  function testLoadMultipleWithDuplicates() {
    var mm = getModuleManager({'a': [], 'b': []});
    mm.setBatchModeEnabled(true);
    mm.setLoader(createSuccessfulBatchLoader(mm));

    var listWithDuplicates = ['a', 'a', 'b'];
    mm.loadMultiple(listWithDuplicates);
    assertArrayEquals('loadMultiple should not modify its input',
        ['a', 'a', 'b'], listWithDuplicates);
  }

  /**
   * Test loading dependencies transitively.
   */
  function testLoadingDepsInNonBatchMode1() {
    var mm = getModuleManager({
      'i': [],
      'j': [],
      'k': ['j'],
      'l': ['i','j','k']});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    mm.preloadModule('j');
    clock.tick(5);
    assertTrue('module "j" should be loaded', mm.isModuleLoaded('j'));
    assertFalse(
        'module "i" should not be loaded (1)', mm.isModuleLoaded('i'));
    assertFalse(
        'module "k" should not be loaded (1)', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (1)', mm.isModuleLoaded('l'));

    // When loading a module in non-batch mode, its dependencies should be
    // requested independently, and in dependency order.
    mm.preloadModule('l');
    clock.tick(5);
    assertTrue('module "i" should be loaded', mm.isModuleLoaded('i'));
    assertFalse(
        'module "k" should not be loaded (2)', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (2)', mm.isModuleLoaded('l'));
    clock.tick(5);
    assertTrue('module "k" should be loaded', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (3)', mm.isModuleLoaded('l'));
    clock.tick(5);
    assertTrue(
        'module "l" should be loaded', mm.isModuleLoaded('l'));
  }

  /**
   * Test loading dependencies transitively and in dependency order.
   */
  function testLoadingDepsInNonBatchMode2() {
    var mm = getModuleManager({
      'h': [],
      'i': ['h'],
      'j': ['i'],
      'k': ['j'],
      'l': ['i','j','k'],
      'm': ['l']});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    // When loading a module in non-batch mode, its dependencies should be
    // requested independently, and in dependency order. The order in this
    // case should be h,i,j,k,l,m.
    mm.preloadModule('m');
    clock.tick(5);
    assertTrue('module "h" should be loaded', mm.isModuleLoaded('h'));
    assertFalse(
        'module "i" should not be loaded (1)', mm.isModuleLoaded('i'));
    assertFalse(
        'module "j" should not be loaded (1)', mm.isModuleLoaded('j'));
    assertFalse(
        'module "k" should not be loaded (1)', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (1)', mm.isModuleLoaded('l'));
    assertFalse(
        'module "m" should not be loaded (1)', mm.isModuleLoaded('m'));

    clock.tick(5);
    assertTrue('module "i" should be loaded', mm.isModuleLoaded('i'));
    assertFalse(
        'module "j" should not be loaded (2)', mm.isModuleLoaded('j'));
    assertFalse(
        'module "k" should not be loaded (2)', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (2)', mm.isModuleLoaded('l'));
    assertFalse(
        'module "m" should not be loaded (2)', mm.isModuleLoaded('m'));

    clock.tick(5);
    assertTrue('module "j" should be loaded', mm.isModuleLoaded('j'));
    assertFalse(
        'module "k" should not be loaded (3)', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (3)', mm.isModuleLoaded('l'));
    assertFalse(
        'module "m" should not be loaded (3)', mm.isModuleLoaded('m'));

    clock.tick(5);
    assertTrue('module "k" should be loaded', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (4)', mm.isModuleLoaded('l'));
    assertFalse(
        'module "m" should not be loaded (4)', mm.isModuleLoaded('m'));

    clock.tick(5);
    assertTrue('module "l" should be loaded', mm.isModuleLoaded('l'));
    assertFalse(
        'module "m" should not be loaded (5)', mm.isModuleLoaded('m'));

    clock.tick(5);
    assertTrue('module "m" should be loaded', mm.isModuleLoaded('m'));
  }

  function testLoadingDepsInBatchMode() {
    var mm = getModuleManager({
      'e': [],
      'f': [],
      'g': ['f'],
      'h': ['e','f','g']});
    mm.setLoader(createSuccessfulBatchLoader(mm));
    mm.setBatchModeEnabled(true);

    mm.preloadModule('f');
    clock.tick(5);
    assertTrue('module "f" should be loaded', mm.isModuleLoaded('f'));
    assertFalse(
        'module "e" should not be loaded (1)', mm.isModuleLoaded('e'));
    assertFalse(
        'module "g" should not be loaded (1)', mm.isModuleLoaded('g'));
    assertFalse(
        'module "h" should not be loaded (1)', mm.isModuleLoaded('h'));

    // When loading a module in batch mode, its not-yet-loaded dependencies
    // should be requested at the same time, and in dependency order.
    mm.preloadModule('h');
    clock.tick(5);
    assertTrue('module "e" should be loaded', mm.isModuleLoaded('e'));
    assertFalse(
        'module "g" should not be loaded (2)', mm.isModuleLoaded('g'));
    assertFalse(
        'module "h" should not be loaded (2)', mm.isModuleLoaded('h'));
    clock.tick(2);
    assertTrue(
        'module "g" should be loaded', mm.isModuleLoaded('g'));
    assertFalse(
        'module "h" should not be loaded (3)', mm.isModuleLoaded('h'));
    clock.tick(2);
    assertTrue(
        'module "h" should be loaded', mm.isModuleLoaded('h'));
  }

  /**
   * Test unauthorized errors while loading modules.
   */
  function testUnauthorizedLoading() {
    var mm = getModuleManager({
      'm': [],
      'n': [],
      'o': ['n']});
    mm.setLoader(createUnsuccessfulLoader(mm, 401));

    // Callback checks for an unauthorized error
    var firedLoadFailed = false;
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
          assertEquals('Failure cause was not as expected',
                       goog.module.ModuleManager.FailureType.UNAUTHORIZED,
                       cause);
          firedLoadFailed = true;
        });
    mm.execOnLoad('o', function() {});
    assertTrue('module "o" should be loading', mm.isModuleLoading('o'));
    assertTrue('module "n" should be loading', mm.isModuleLoading('n'));
    clock.tick(5);
    assertTrue(
        'should have called unauthorized module callback', firedLoadFailed);
    assertFalse(
        'module "o" should not be loaded', mm.isModuleLoaded('o'));
    assertFalse(
        'module "o" should not be loading', mm.isModuleLoading('o'));
    assertFalse(
        'module "n" should not be loaded', mm.isModuleLoaded('n'));
    assertFalse(
        'module "n" should not be loading', mm.isModuleLoading('n'));
  }

  /**
   * Test error loading modules which are retried.
   */
  function testErrorLoadingModule() {
    var mm = getModuleManager({
      'p': ['q'],
      'q': [],
      'r': ['q','p']});
    mm.setLoader(createUnsuccessfulLoader(mm, 500));

    mm.preloadModule('r');
    clock.tick(4);

    // A module request is now underway using the unsuccessful loader.
    // We substitute a successful loader for future module load requests.
    mm.setLoader(createSuccessfulNonBatchLoader(mm));
    clock.tick(1);
    assertFalse(
        'module "q" should not be loaded (1)', mm.isModuleLoaded('q'));
    assertFalse(
        'module "p" should not be loaded (1)', mm.isModuleLoaded('p'));
    assertFalse(
        'module "r" should not be loaded (1)', mm.isModuleLoaded('r'));

    // Failed loads are automatically retried after a backOff.
    clock.tick(5 + mm.getBackOff_());
    assertTrue('module "q" should be loaded', mm.isModuleLoaded('q'));
    assertFalse(
        'module "p" should not be loaded (2)', mm.isModuleLoaded('p'));
    assertFalse(
        'module "r" should not be loaded (2)', mm.isModuleLoaded('r'));

    // A successful load decrements the backOff.
    clock.tick(5);
    assertTrue('module "p" should be loaded', mm.isModuleLoaded('p'));
    assertFalse(
        'module "r" should not be loaded (3)', mm.isModuleLoaded('r'));
    clock.tick(5);
    assertTrue(
        'module "r" should be loaded', mm.isModuleLoaded('r'));
  }

  /**
   * Tests error loading modules which are retried.
   */
  function testErrorLoadingModule_batchMode() {
    var mm = getModuleManager({
      'p': ['q'],
      'q': [],
      'r': ['q','p']});
    mm.setLoader(createUnsuccessfulBatchLoader(mm, 500));
    mm.setBatchModeEnabled(true);

    mm.preloadModule('r');
    clock.tick(4);

    // A module request is now underway using the unsuccessful loader.
    // We substitute a successful loader for future module load requests.
    mm.setLoader(createSuccessfulBatchLoader(mm));
    clock.tick(1);
    assertFalse(
        'module "q" should not be loaded (1)', mm.isModuleLoaded('q'));
    assertFalse(
        'module "p" should not be loaded (1)', mm.isModuleLoaded('p'));
    assertFalse(
        'module "r" should not be loaded (1)', mm.isModuleLoaded('r'));

    // Failed loads are automatically retried after a backOff.
    clock.tick(5 + mm.getBackOff_());
    assertTrue('module "q" should be loaded', mm.isModuleLoaded('q'));
    clock.tick(2);
    assertTrue(
        'module "p" should not be loaded (2)', mm.isModuleLoaded('p'));
    clock.tick(2);
    assertTrue(
        'module "r" should not be loaded (2)', mm.isModuleLoaded('r'));
  }

  /**
   * Test consecutive errors in loading modules.
   */
  function testConsecutiveErrors() {
    var mm = getModuleManager({'s': []});
    mm.setLoader(createUnsuccessfulLoader(mm, 500));

    // Register an error callback for consecutive failures.
    var firedLoadFailed = false;
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
          assertEquals('Failure cause was not as expected',
              goog.module.ModuleManager.FailureType.CONSECUTIVE_FAILURES,
              cause);
          firedLoadFailed = true;
        });

    mm.preloadModule('s');
    assertFalse(
        'module "s" should not be loaded (0)', mm.isModuleLoaded('s'));

    // Fail twice.
    for (var i = 0; i < 2; i++) {
      clock.tick(5 + mm.getBackOff_());
      assertFalse(
          'module "s" should not be loaded (1)', mm.isModuleLoaded('s'));
      assertFalse(
          'should not fire failed callback (1)', firedLoadFailed);
    }

    // Fail a third time and check that the callback is fired.
    clock.tick(5 + mm.getBackOff_());
    assertFalse(
        'module "s" should not be loaded (2)', mm.isModuleLoaded('s'));
    assertTrue(
        'should have fired failed callback', firedLoadFailed);

    // Check that it doesn't attempt to load the module anymore after it has
    // failed.
    var triedLoad = false;
    mm.setLoader({
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn) {
        triedLoad = true;
      }});

    // Also reset the failed callback flag and make sure it isn't called
    // again.
    firedLoadFailed = false;
    clock.tick(10 + mm.getBackOff_());
    assertFalse(
        'module "s" should not be loaded (3)', mm.isModuleLoaded('s'));
    assertFalse('No more loads should have been tried', triedLoad);
    assertFalse('The load failed callback should be fired only once',
        firedLoadFailed);
  }

  /**
   * Test loading errors due to old code.
   */
  function testOldCodeGoneError() {
    var mm = getModuleManager({'s': []});
    mm.setLoader(createUnsuccessfulLoader(mm, 410));

    // Callback checks for an old code failure
    var firedLoadFailed = false;
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
          assertEquals('Failure cause was not as expected',
              goog.module.ModuleManager.FailureType.OLD_CODE_GONE,
              cause);
          firedLoadFailed = true;
        });

    mm.preloadModule('s', 0);
    assertFalse(
        'module "s" should not be loaded (0)', mm.isModuleLoaded('s'));
    clock.tick(5);
    assertFalse(
        'module "s" should not be loaded (1)', mm.isModuleLoaded('s'));
    assertTrue(
        'should have called old code gone callback', firedLoadFailed);
  }

  /**
   * Test timeout.
   */
  function testTimeout() {
    var mm = getModuleManager({'s': []});
    mm.setLoader(createTimeoutLoader(mm));

    // Callback checks for timeout
    var firedTimeout = false;
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
          assertEquals('Failure cause was not as expected',
              goog.module.ModuleManager.FailureType.TIMEOUT,
              cause);
          firedTimeout = true;
        });

    mm.preloadModule('s', 0);
    assertFalse(
        'module "s" should not be loaded (0)', mm.isModuleLoaded('s'));
    clock.tick(5);
    assertFalse(
        'module "s" should not be loaded (1)', mm.isModuleLoaded('s'));
    assertTrue(
        'should have called timeout callback', firedTimeout);
  }

  /**
   * Tests that an error during execOnLoad will trigger the error callback.
   */
  function testExecOnLoadError() {
    // Expect two callbacks, each of which will be called with callback type
    // ERROR, the right module id and failure type INIT_ERROR.
    var errorCallback1 = goog.testing.createFunctionMock('callback1');
    errorCallback1(goog.module.ModuleManager.CallbackType.ERROR, 'b',
        goog.module.ModuleManager.FailureType.INIT_ERROR);

    var errorCallback2 = goog.testing.createFunctionMock('callback2');
    errorCallback2(goog.module.ModuleManager.CallbackType.ERROR, 'b',
        goog.module.ModuleManager.FailureType.INIT_ERROR);

    errorCallback1.$replay();
    errorCallback2.$replay();

    var mm = new goog.module.ModuleManager();
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    // Register the first callback before setting the module info map.
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        errorCallback1);

    mm.setAllModuleInfo({'a': [], 'b': [], 'c': []});

    // Register the second callback after setting the module info map.
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        errorCallback2);

    var execOnLoadBCalled = false;
    mm.execOnLoad('b', function() {
      execOnLoadBCalled = true;
      throw new Error();
    });

    clock.tick(5);

    assertTrue('execOnLoad should have been called on module b.',
        execOnLoadBCalled);
    errorCallback1.$verify();
    errorCallback2.$verify();
  }

  /**
   * Tests that an error during execOnLoad will trigger the error callback.
   * Uses setAllModuleInfoString rather than setAllModuleInfo.
   */
  function testExecOnLoadErrorModuleInfoString() {
    // Expect a callback to be called with callback type ERROR, the right module
    // id and failure type INIT_ERROR.
    var errorCallback = goog.testing.createFunctionMock('callback');
    errorCallback(goog.module.ModuleManager.CallbackType.ERROR, 'b',
        goog.module.ModuleManager.FailureType.INIT_ERROR);

    errorCallback.$replay();

    var mm = new goog.module.ModuleManager();
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    // Register the first callback before setting the module info map.
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        errorCallback);

    mm.setAllModuleInfoString('a/b/c');

    var execOnLoadBCalled = false;
    mm.execOnLoad('b', function() {
      execOnLoadBCalled = true;
      throw new Error();
    });

    clock.tick(5);

    assertTrue('execOnLoad should have been called on module b.',
        execOnLoadBCalled);
    errorCallback.$verify();
  }

  /**
   * Make sure ModuleInfo objects in moduleInfoMap_ get disposed.
   */
  function testDispose() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});

    var moduleInfoA = mm.getModuleInfo('a');
    assertNotNull(moduleInfoA);
    var moduleInfoB = mm.getModuleInfo('b');
    assertNotNull(moduleInfoB);
    var moduleInfoC = mm.getModuleInfo('c');
    assertNotNull(moduleInfoC);

    mm.dispose();
    assertTrue(moduleInfoA.isDisposed());
    assertTrue(moduleInfoB.isDisposed());
    assertTrue(moduleInfoC.isDisposed());
  }

  function testDependencyOrderingWithSimpleDeps() {
    var mm = getModuleManager({
      'a': ['b', 'c'],
      'b': ['d'],
      'c': ['e', 'f'],
      'd': [],
      'e': [],
      'f': []
    });
    var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
    assertDependencyOrder(ids, mm);
    assertArrayEquals(['d', 'e', 'f', 'b', 'c', 'a'], ids);
  }

  function testDependencyOrderingWithCommonDepsInDeps() {
    // Tests to make sure that if dependencies of the root are loaded before
    // their common dependencies.
    var mm = getModuleManager({
      'a': ['b', 'c'],
      'b': ['d'],
      'c': ['d'],
      'd': []
    });
    var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
    assertDependencyOrder(ids, mm);
    assertArrayEquals(['d', 'b', 'c', 'a'], ids);
  }

  function testDependencyOrderingWithCommonDepsInRoot1() {
    // Tests the case where a dependency of the root depends on another
    // dependency of the root.  Irregardless of ordering in the root's
    // deps.
    var mm = getModuleManager({
      'a': ['b', 'c'],
      'b': ['c'],
      'c': []
    });
    var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
    assertDependencyOrder(ids, mm);
    assertArrayEquals(['c', 'b', 'a'], ids);
  }

  function testDependencyOrderingWithCommonDepsInRoot2() {
    // Tests the case where a dependency of the root depends on another
    // dependency of the root.  Irregardless of ordering in the root's
    // deps.
    var mm = getModuleManager({
      'a': ['b', 'c'],
      'b': [],
      'c': ['b']
    });
    var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
    assertDependencyOrder(ids, mm);
    assertArrayEquals(['b', 'c', 'a'], ids);
  }

  function testDependencyOrderingWithGmailExample() {
    // Real dependency graph taken from gmail.
    var mm = getModuleManager({
      's': ['dp', 'ml', 'md'],
      'dp': ['a'],
      'ml': ['ld', 'm'],
      'ld': ['a'],
      'm': ['ad', 'mh', 'n'],
      'md': ['mh', 'ld'],
      'a': [],
      'mh': [],
      'ad': [],
      'n': []
    });

    mm.setLoaded('a');
    mm.setLoaded('m');
    mm.setLoaded('n');
    mm.setLoaded('ad');
    mm.setLoaded('mh');

    var ids = mm.getNotYetLoadedTransitiveDepIds_('s');
    assertDependencyOrder(ids, mm);
    assertArrayEquals(['ld', 'dp', 'ml', 'md', 's'], ids);
  }

  function assertDependencyOrder(list, mm) {
    var seen = {};
    for (var i = 0; i < list.length; i++) {
      var id = list[i];
      seen[id] = true;
      var deps = mm.getModuleInfo(id).getDependencies();
      for (var j = 0; j < deps.length; j++) {
        var dep = deps[j];
        assertTrue('Unresolved dependency [' + dep + '] for [' + id + '].',
            seen[dep] || mm.getModuleInfo(dep).isLoaded());
      }
    }
  }

 function testRegisterInitializationCallback() {
   var initCalled = 0;
   var mm = getModuleManager({'a': [], 'b': [], 'c': []});
   mm.setLoader(createSuccessfulNonBatchLoaderWithRegisterInitCallback(mm,
       function() {
        ++initCalled;
       }));
   execOnLoad_(mm);
   // execOnLoad_ loads modules a and c
   assertTrue(initCalled == 2);
 }

  function createSuccessfulNonBatchLoaderWithRegisterInitCallback(
      moduleMgr, fn) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        moduleMgr.beforeLoadModuleCode(ids[0]);
        moduleMgr.registerInitializationCallback(fn);
        setTimeout(function() {
          moduleMgr.setLoaded(ids[0]);
          moduleMgr.afterLoadModuleCode(ids[0]);
          if (opt_successFn) {
            opt_successFn();
          }
        }, 5);
      }};
  }

  function testSetModuleConstructor() {
    var initCalled = 0;
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    var info = {
      'a': { ctor: AModule, count: 0 },
      'b': { ctor: BModule, count: 0 },
      'c': { ctor: CModule, count: 0 }
    };
    function AModule() {
      ++info['a'].count;
      goog.module.BaseModule.call(this);
    }
    goog.inherits(AModule, goog.module.BaseModule);
    function BModule() {
      ++info['b'].count;
      goog.module.BaseModule.call(this);
    }
    goog.inherits(BModule, goog.module.BaseModule);
    function CModule() {
      ++info['c'].count;
      goog.module.BaseModule.call(this);
    }
    goog.inherits(CModule, goog.module.BaseModule);

    mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(mm, info));
    execOnLoad_(mm);
    assertTrue(info['a'].count == 1);
    assertTrue(info['b'].count == 0);
    assertTrue(info['c'].count == 1);
    assertTrue(mm.getModuleInfo('a').getModule() instanceof AModule);
    assertTrue(mm.getModuleInfo('c').getModule() instanceof CModule);
  }

  /**
   * Tests that a call to load the loading module during module initialization
   * doesn't trigger a second load.
   */
  function testLoadWhenInitializing() {
    var mm = getModuleManager({'a': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var info = {
      'a': { ctor: AModule, count: 0 }
    };
    function AModule() {
      ++info['a'].count;
      goog.module.BaseModule.call(this);
    }
    goog.inherits(AModule, goog.module.BaseModule);
    AModule.prototype.initialize = function() {
      mm.load('a');
    };
    mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(mm, info));
    mm.preloadModule('a');
    clock.tick(5);
    assertEquals(info['a'].count, 1);
  }

  function testErrorInEarlyCallback() {
    var errback = goog.testing.recordFunction();
    var callback = goog.testing.recordFunction();
    var mm = getModuleManager({'a': [], 'b': ['a']});
    mm.getModuleInfo('a').registerEarlyCallback(goog.functions.error('error'));
    mm.getModuleInfo('a').registerCallback(callback);
    mm.getModuleInfo('a').registerErrback(errback);

    mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(
        mm, createModulesFor('a', 'b')));
    mm.preloadModule('b');
    clock.tick(5);

    assertEquals(0, callback.getCallCount());
    assertEquals(1, errback.getCallCount());
    assertEquals(goog.module.ModuleManager.FailureType.INIT_ERROR,
        errback.getLastCall().getArguments()[0]);
    assertTrue(mm.getModuleInfo('a').isLoaded());
    assertFalse(mm.getModuleInfo('b').isLoaded());

    clock.tick(5);
    assertTrue(mm.getModuleInfo('b').isLoaded());
  }

  function testErrorInNormalCallback() {
    var earlyCallback = goog.testing.recordFunction();
    var errback = goog.testing.recordFunction();
    var mm = getModuleManager({'a': [], 'b': ['a']});
    mm.getModuleInfo('a').registerEarlyCallback(earlyCallback);
    mm.getModuleInfo('a').registerEarlyCallback(goog.functions.error('error'));
    mm.getModuleInfo('a').registerErrback(errback);

    mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(
        mm, createModulesFor('a', 'b')));
    mm.preloadModule('b');
    clock.tick(10);

    assertEquals(1, errback.getCallCount());
    assertEquals(goog.module.ModuleManager.FailureType.INIT_ERROR,
        errback.getLastCall().getArguments()[0]);
    assertTrue(mm.getModuleInfo('a').isLoaded());
    assertTrue(mm.getModuleInfo('b').isLoaded());
  }

  function testErrorInErrback() {
    var mm = getModuleManager({'a': [], 'b': ['a']});
    mm.getModuleInfo('a').registerCallback(goog.functions.error('error1'));
    mm.getModuleInfo('a').registerErrback(goog.functions.error('error2'));

    mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(
        mm, createModulesFor('a', 'b')));
    mm.preloadModule('a');
    var e = assertThrows(function() {
      clock.tick(10);
    });
    assertContains('Module errback failure', e.message);
    if (!goog.userAgent.IE) {
      assertContains('error2', e.message);
    }

    assertTrue(mm.getModuleInfo('a').isLoaded());
  }

  function createModulesFor(var_args) {
    var result = {};
    for (var i = 0; i < arguments.length; i++) {
      var key = arguments[i];
      result[key] = {ctor: goog.module.BaseModule};
    }
    return result;
  }

  function createSuccessfulNonBatchLoaderWithConstructor(moduleMgr, info) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        setTimeout(function() {
          moduleMgr.beforeLoadModuleCode(ids[0]);
          moduleMgr.setModuleConstructor(info[ids[0]].ctor);
          moduleMgr.setLoaded(ids[0]);
          moduleMgr.afterLoadModuleCode(ids[0]);
          if (opt_successFn) {
            opt_successFn();
          }
        }, 5);
      }};
  }

  function testInitCallbackInBaseModule() {
    var mm = new goog.module.ModuleManager();
    var called = false;
    var context;
    mm.registerInitializationCallback(function(mcontext) {
      called = true;
      context = mcontext;
    });
    mm.setAllModuleInfo({'a': [], 'b': ['a']});
    assertTrue('Base initialization not called', called);
    assertNull('Context should still be null', context);

    var mm = new goog.module.ModuleManager();
    called = false;
    mm.registerInitializationCallback(function(mcontext) {
      called = true;
      context = mcontext;
    });
    var appContext = {};
    mm.setModuleContext(appContext);
    assertTrue('Base initialization not called after setModuleContext', called);
    assertEquals('Did not receive module context', appContext, context);
  }

  function testSetAllModuleInfoString() {
    var info = 'base/one:0/two:0/three:0,1,2/four:0,3/five:';
    var mm = new goog.module.ModuleManager();
    mm.setAllModuleInfoString(info);

    assertNotNull('Base should exist', mm.getModuleInfo('base'));
    assertNotNull('One should exist', mm.getModuleInfo('one'));
    assertNotNull('Two should exist', mm.getModuleInfo('two'));
    assertNotNull('Three should exist', mm.getModuleInfo('three'));
    assertNotNull('Four should exist', mm.getModuleInfo('four'));
    assertNotNull('Five should exist', mm.getModuleInfo('five'));

    assertArrayEquals(['base', 'one', 'two'],
        mm.getModuleInfo('three').getDependencies());
    assertArrayEquals(['base', 'three'],
        mm.getModuleInfo('four').getDependencies());
    assertArrayEquals([],
        mm.getModuleInfo('five').getDependencies());
  }

  function testSetAllModuleInfoStringWithEmptyString() {
    var mm = new goog.module.ModuleManager();
    var called = false;
    var context;
    mm.registerInitializationCallback(function(mcontext) {
      called = true;
      context = mcontext;
    });
    mm.setAllModuleInfoString('');
    assertTrue('Initialization not called', called);
  }

  function testBackOffAmounts() {
    var mm = new goog.module.ModuleManager();
    assertEquals(0, mm.getBackOff_());

    mm.consecutiveFailures_++;
    assertEquals(5000, mm.getBackOff_());

    mm.consecutiveFailures_++;
    assertEquals(20000, mm.getBackOff_());
  }

  /**
   * Tests that the IDLE callbacks are executed for active->idle transitions
   * after setAllModuleInfoString with currently loading modules.
   */
  function testIdleCallbackWithInitialModules() {
    var callback = goog.testing.recordFunction();

    var mm = new goog.module.ModuleManager();
    mm.setAllModuleInfoString('a', ['a']);
    mm.registerCallback(
        goog.module.ModuleManager.CallbackType.IDLE, callback);

    assertTrue(mm.isActive());

    mm.beforeLoadModuleCode('a');

    assertEquals(0, callback.getCallCount());

    mm.setLoaded('a');
    mm.afterLoadModuleCode('a');

    assertFalse(mm.isActive());

    assertEquals(1, callback.getCallCount());
  }
</script>
</body>
</html>
